All files / web/src/app/api/integrations/kid-songs/[playerId]/latest route.ts

94.31% Statements 83/88
88.46% Branches 23/26
100% Functions 3/3
94.31% Lines 83/88

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 891x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 15x 15x 15x 15x 15x 15x 1x 2x 2x 2x 2x 1x 1x 1x 2x       2x 1x 15x 15x 15x 15x 15x 15x 15x 7x 7x 7x 15x 2x 2x 5x 15x 15x 1x 1x 4x 4x 4x 15x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x     1x 3x 3x 3x 1x 2x 15x 15x 1x 1x 1x 15x  
import { authorizeKidSongsRequest } from '@/lib/auth/integrationToken'
import { readAndHashSong } from '@/lib/kid-songs/audio'
import {
  audioUrlFor,
  isValidId,
  toEligibleSongCandidate,
  toIso,
  toNullableIso,
} from '@/lib/kid-songs/eligibility'
import { getLatestSongCandidates, getSongCandidateById } from '@/lib/kid-songs/queries'
 
export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'
 
const headers = { 'Cache-Control': 'no-store', Vary: 'Authorization' }
function json(body: unknown, status = 200): Response {
  return new Response(JSON.stringify(body), {
    status,
    headers: { ...headers, 'Content-Type': 'application/json' },
  })
}
 
async function isFullyEligible(playerId: string, songId: string): Promise<boolean> {
  const row = await getSongCandidateById(playerId, songId)
  const candidate = row ? toEligibleSongCandidate(row) : null
  if (!candidate) return false
  try {
    await readAndHashSong(candidate.id)
    return true
  } catch (error) {
    console.error(`[kid-songs] Unable to hash known song ${candidate.id}:`, error)
    return false
  }
}
 
export async function GET(
  request: Request,
  context: { params: Promise<{ playerId: string }> }
): Promise<Response> {
  try {
    const auth = authorizeKidSongsRequest(request)
    if (!auth.configured || !auth.tokenAccepted) return json({ error: 'Unauthorized' }, 401)
 
    const { playerId } = await context.params
    const knownSongId = new URL(request.url).searchParams.get('knownSongId')
    if (!isValidId(playerId) || (knownSongId !== null && !isValidId(knownSongId))) {
      return json({ error: 'Invalid parameters' }, 400)
    }
 
    const emptyKnown = knownSongId === null ? null : { id: knownSongId, eligible: false }
    if (!auth.allowedPlayerIds.has(playerId)) {
      return json({ song: null, knownSong: emptyKnown })
    }
 
    let song = null
    const candidates = await getLatestSongCandidates(playerId)
    for (const row of candidates) {
      const candidate = toEligibleSongCandidate(row)
      if (!candidate) continue
      try {
        const audio = await readAndHashSong(candidate.id)
        song = {
          id: candidate.id,
          playerId: candidate.playerId,
          title: candidate.title,
          durationSeconds: candidate.durationSeconds,
          createdAt: toIso(candidate.createdAt),
          completedAt: toNullableIso(candidate.completedAt),
          audioVersion: audio.sha256,
          audioSha256: audio.sha256,
          audioUrl: audioUrlFor(candidate.playerId, candidate.id, audio.sha256),
        }
        break
      } catch (error) {
        console.error(`[kid-songs] Skipping unreadable song ${candidate.id}:`, error)
      }
    }
 
    const knownSong =
      knownSongId === null
        ? null
        : { id: knownSongId, eligible: await isFullyEligible(playerId, knownSongId) }
    return json({ song, knownSong })
  } catch (error) {
    console.error('[kid-songs] Latest route failed:', error)
    return json({ error: 'Internal error' }, 500)
  }
}